/**
 * Copyright (C) 2012 Iordan Iordanov
 * Copyright (C) 2010 Michael A. MacDonald
 * Copyright (C) 2004 Horizon Wimba.  All Rights Reserved.
 * Copyright (C) 2001-2003 HorizonLive.com, Inc.  All Rights Reserved.
 * Copyright (C) 2001,2002 Constantin Kaplinsky.  All Rights Reserved.
 * Copyright (C) 2000 Tridia Corporation.  All Rights Reserved.
 * Copyright (C) 1999 AT&T Laboratories Cambridge.  All Rights Reserved.
 * <p>
 * This is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * <p>
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * <p>
 * You should have received a copy of the GNU General Public License
 * along with this software; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
 * USA.
 */

//
// RemoteCanvas is a subclass of android.view.SurfaceView which draws a VNC
// desktop on it.
//

package com.iiordanov.bVNC;

import java.io.IOException;
import java.net.Socket;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Locale;
import java.util.Timer;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.provider.Settings;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.text.ClipboardManager;
import android.text.InputType;
import android.util.AttributeSet;
import android.util.Base64;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.KeyEvent;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.Toast;

import com.freerdp.freerdpcore.application.GlobalApp;
import com.freerdp.freerdpcore.application.SessionState;
import com.freerdp.freerdpcore.domain.BookmarkBase;
import com.freerdp.freerdpcore.domain.ManualBookmark;
import com.freerdp.freerdpcore.services.LibFreeRDP;
import com.iiordanov.android.bc.BCFactory;
import com.iiordanov.bVNC.input.InputHandlerTouchpad;
import com.iiordanov.bVNC.input.RemoteKeyboard;
import com.iiordanov.bVNC.input.RemotePointer;
import com.iiordanov.bVNC.input.RemoteRdpKeyboard;
import com.iiordanov.bVNC.input.RemoteRdpPointer;
import com.iiordanov.bVNC.input.RemoteSpiceKeyboard;
import com.iiordanov.bVNC.input.RemoteSpicePointer;
import com.iiordanov.bVNC.input.RemoteVncKeyboard;
import com.iiordanov.bVNC.input.RemoteVncPointer;

import com.iiordanov.bVNC.dialogs.GetTextFragment;
import com.iiordanov.bVNC.exceptions.AnonCipherUnsupportedException;
import com.undatech.opaque.RdpCommunicator;
import com.undatech.opaque.RfbConnectable;
import com.undatech.opaque.Viewable;
import com.undatech.opaque.SpiceCommunicator;

import com.iiordanov.bVNC.*;
import com.iiordanov.freebVNC.*;
import com.iiordanov.aRDP.*;
import com.iiordanov.freeaRDP.*;
import com.iiordanov.aSPICE.*;
import com.iiordanov.freeaSPICE.*;

public class RemoteCanvas extends android.support.v7.widget.AppCompatImageView implements Viewable {
    private final static String TAG = "RemoteCanvas";

    public AbstractScaling canvasZoomer;

    // Variable indicating that we are currently scrolling in simulated touchpad mode.
    public boolean cursorBeingMoved = false;

    // Connection parameters
    ConnectionBean connection;
    Database database;
    private SSHConnection sshConnection = null;

    // VNC protocol connection
    public RfbConnectable rfbconn = null;
    private RfbProto rfb = null;
    private RdpCommunicator rdpcomm = null;
    private SpiceCommunicator spicecomm = null;
    private Socket sock = null;

    boolean maintainConnection = true;

    // RFB Decoder
    Decoder decoder = null;

    // The remote pointer and keyboard
    RemotePointer pointer;
    RemoteKeyboard keyboard;

    // Internal bitmap data
    private int capacity;
    public AbstractBitmapData myDrawable;
    boolean useFull = false;
    boolean compact = false;

    // Keeps track of libFreeRDP instance. 
    GlobalApp freeRdpApp = null;
    SessionState session = null;

    // Progress dialog shown at connection time.
    ProgressDialog pd;

    // Used to set the contents of the clipboard.
    ClipboardManager clipboard;
    Timer clipboardMonitorTimer;
    ClipboardMonitor clipboardMonitor;
    public boolean serverJustCutText = false;

    private Runnable setModes;

    /*
     * Position of the top left portion of the <i>visible</i> part of the screen, in
     * full-frame coordinates
     */
    int absoluteXPosition = 0, absoluteYPosition = 0;

    /*
     * How much to shift coordinates over when converting from full to view coordinates.
     */
    float shiftX = 0, shiftY = 0;

    /*
     * This variable holds the height of the visible rectangle of the screen. It is used to keep track
     * of how much of the screen is hidden by the soft keyboard if any.
     */
    int visibleHeight = -1;

    /*
     * These variables contain the width and height of the display in pixels
     */
    int displayWidth = 0;
    int displayHeight = 0;
    float displayDensity = 0;

    /*
     * This flag indicates whether this is the VNC client.
     */
    boolean isVnc = false;

    /*
     * This flag indicates whether this is the RDP client.
     */
    boolean isRdp = false;
    BookmarkBase bookmark;

    /*
     * This flag indicates whether this is the SPICE client.
     */
    boolean isSpice = false;
    boolean spiceUpdateReceived = false;

    /*
     * Variable used for BB workarounds.
     */
    boolean bb = false;

    boolean sshTunneled = false;

    /**
     * Constructor used by the inflation apparatus
     *
     * @param context
     */
    public RemoteCanvas(final Context context, AttributeSet attrs) {
        super(context, attrs);

        clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);

        decoder = new Decoder(this);

        isVnc = getContext().getPackageName().contains("VNC");
        isRdp = getContext().getPackageName().contains("RDP");
        isSpice = getContext().getPackageName().contains("SPICE");

        final Display display = ((Activity) context).getWindow().getWindowManager().getDefaultDisplay();
        displayWidth = display.getWidth();
        displayHeight = display.getHeight();
        DisplayMetrics metrics = new DisplayMetrics();
        display.getMetrics(metrics);
        displayDensity = metrics.density;

        if (android.os.Build.MODEL.contains("BlackBerry") ||
                android.os.Build.BRAND.contains("BlackBerry") ||
                android.os.Build.MANUFACTURER.contains("BlackBerry")) {
            bb = true;
        }
    }


    /**
     * Create a view showing a remote desktop connection
     *
     * @param bean     Connection settings
     * @param setModes Callback to run on UI thread after connection is set up
     */
    RemotePointer initializeCanvas(ConnectionBean bean, Database db, final Runnable setModes) {
        this.setModes = setModes;
        connection = bean;
        database = db;
        decoder.setColorModel(COLORMODEL.valueOf(bean.getColorModel()));

        sshTunneled = (connection.getConnectionType() == Constants.CONN_TYPE_SSH);

        // Startup the connection thread with a progress dialog
        pd = ProgressDialog.show(getContext(), getContext().getString(R.string.info_progress_dialog_connecting),
                getContext().getString(R.string.info_progress_dialog_establishing),
                true, true, new DialogInterface.OnCancelListener() {
                    @Override
                    public void onCancel(DialogInterface dialog) {
                        closeConnection();
                        handler.post(new Runnable() {
                            public void run() {
                                Utils.showFatalErrorMessage(getContext(), getContext().getString(R.string.info_progress_dialog_aborted));
                            }
                        });
                    }
                });

        // Make this dialog cancellable only upon hitting the Back button and not touching outside.
        pd.setCanceledOnTouchOutside(false);

        try {
            if (isSpice) {
                initializeSpiceConnection();
            } else if (isRdp) {
                initializeRdpConnection();
            } else {
                initializeVncConnection();
            }
        } catch (Throwable e) {
            handleUncaughtException(e);
        }

        Thread t = new Thread() {
            public void run() {
                try {
                    // Initialize SSH key if necessary
                    if (sshTunneled && connection.getSshHostKey().equals("") &&
                            Utils.isNullOrEmptry(connection.getIdHash())) {
                        handler.sendEmptyMessage(Constants.DIALOG_SSH_CERT);

                        // Block while user decides whether to accept certificate or not.
                        // The activity ends if the user taps "No", so we block indefinitely here.
                        synchronized (RemoteCanvas.this) {
                            while (connection.getSshHostKey().equals("")) {
                                try {
                                    RemoteCanvas.this.wait();
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }
                        }
                    }

                    if (isSpice) {
                        startSpiceConnection();
                    } else if (isRdp) {
                        startRdpConnection();
                    } else {
                        startVncConnection();
                    }
                } catch (Throwable e) {
                    handleUncaughtException(e);
                }
            }
        };
        t.start();

        clipboardMonitor = new ClipboardMonitor(getContext(), this);
        if (clipboardMonitor != null) {
            clipboardMonitorTimer = new Timer();
            if (clipboardMonitorTimer != null) {
                try {
                    clipboardMonitorTimer.schedule(clipboardMonitor, 0, 500);
                } catch (NullPointerException e) {
                }
            }
        }

        return pointer;
    }

    private void handleUncaughtException(Throwable e) {
        if (maintainConnection) {
            Log.e(TAG, e.toString());
            e.printStackTrace();
            // Ensure we dismiss the progress dialog before we finish
            if (pd.isShowing())
                pd.dismiss();

            if (e instanceof OutOfMemoryError) {
                disposeDrawable();
                showFatalMessageAndQuit(getContext().getString(R.string.error_out_of_memory));
            } else {
                String error = getContext().getString(R.string.error_connection_failed);
                if (e.getMessage() != null) {
                    if (e.getMessage().indexOf("SSH") < 0 &&
                            (e.getMessage().indexOf("authentication") > -1 ||
                                    e.getMessage().indexOf("Unknown security result") > -1 ||
                                    e.getMessage().indexOf("password check failed") > -1)
                    ) {
                        error = getContext().getString(R.string.error_vnc_authentication);
                    }
                    error = error + "<br>" + e.getLocalizedMessage();
                }
                showFatalMessageAndQuit(error);
            }
        }
    }


    /**
     * Retreives the requested remote width.
     */
    @Override
    public int getDesiredWidth() {
        int w = getWidth();
        android.util.Log.e(TAG, "Width requested: " + w);
        return w;
    }

    /**
     * Retreives the requested remote height.
     */
    @Override
    public int getDesiredHeight() {
        int h = getHeight();
        android.util.Log.e(TAG, "Height requested: " + h);
        return h;
    }


    /**
     * Initializes a SPICE connection.
     *
     */
    private void initializeSpiceConnection() throws Exception {
        spicecomm = new SpiceCommunicator(getContext(), handler, this, true, true);
        rfbconn = spicecomm;
        pointer = new RemoteSpicePointer(rfbconn, RemoteCanvas.this, handler);
        keyboard = new RemoteSpiceKeyboard(getResources(), spicecomm, RemoteCanvas.this,
                handler, connection.getLayoutMap());
        //spicecomm.setUIEventListener(RemoteCanvas.this);
        spicecomm.setHandler(handler);
    }

    /**
     * Starts a SPICE connection using libspice.
     *
     */
    private void startSpiceConnection() throws Exception {
        // Get the address and port (based on whether an SSH tunnel is being established or not).
        String address = getAddress();
        // To prevent an SSH tunnel being created when port or TLS port is not set, we only
        // getPort when port/tport are positive.
        int port = connection.getPort();
        if (port > 0) {
            port = getPort(port);
        }

        int tport = connection.getTlsPort();
        if (tport > 0) {
            tport = getPort(tport);
        }

        spicecomm.connectSpice(address, Integer.toString(port), Integer.toString(tport), connection.getPassword(),
                connection.getCaCertPath(), null, // TODO: Can send connection.getCaCert() here instead
                connection.getCertSubject(), connection.getEnableSound());
    }

    /**
     * Initializes an RDP connection.
     */
    private void initializeRdpConnection() throws Exception {
        // This is necessary because it initializes a synchronizedMap referenced later.
        freeRdpApp = new GlobalApp();

        // Create a manual bookmark and populate it from settings.
        bookmark = new ManualBookmark();

        // Create a session based on the bookmark
        session = GlobalApp.createSession(bookmark, this.getContext());

        rdpcomm = new RdpCommunicator(session, handler, this, connection.getUserName(), connection.getPassword(), connection.getRdpDomain());
        rfbconn = rdpcomm;
        pointer = new RemoteRdpPointer(rfbconn, RemoteCanvas.this, handler);
        keyboard = new RemoteRdpKeyboard(rfbconn, RemoteCanvas.this, handler);

        session.setUIEventListener(rdpcomm);
        LibFreeRDP.setEventListener(rdpcomm);
    }

    /**
     * Starts an RDP connection using the FreeRDP library.
     */
    private void startRdpConnection() throws Exception {
        // Set a writable data directory
        //LibFreeRDP.setDataDirectory(session.getInstance(), getContext().getFilesDir().toString());
        // Get the address and port (based on whether an SSH tunnel is being established or not).
        String address = getAddress();
        int rdpPort = getPort(connection.getPort());

        bookmark.<ManualBookmark>get().setLabel(connection.getNickname());
        bookmark.<ManualBookmark>get().setHostname(address);
        bookmark.<ManualBookmark>get().setPort(rdpPort);

        BookmarkBase.DebugSettings debugSettings = session.getBookmark().getDebugSettings();
        debugSettings.setDebugLevel("INFO");
        //debugSettings.setAsyncUpdate(false);
        //debugSettings.setAsyncInput(false);
        //debugSettings.setAsyncChannel(false);

        // Set screen settings to native res if instructed to, or if height or width are too small.
        BookmarkBase.ScreenSettings screenSettings = session.getBookmark().getActiveScreenSettings();
        waitUntilInflated();
        int remoteWidth = getRemoteWidth(getWidth(), getHeight());
        int remoteHeight = getRemoteHeight(getWidth(), getHeight());
        screenSettings.setWidth(remoteWidth);
        screenSettings.setHeight(remoteHeight);
        screenSettings.setColors(16);

        // Set performance flags.
        BookmarkBase.PerformanceFlags performanceFlags = session.getBookmark().getPerformanceFlags();
        performanceFlags.setRemoteFX(false);
        performanceFlags.setWallpaper(connection.getDesktopBackground());
        performanceFlags.setFontSmoothing(connection.getFontSmoothing());
        performanceFlags.setDesktopComposition(connection.getDesktopComposition());
        performanceFlags.setFullWindowDrag(connection.getWindowContents());
        performanceFlags.setMenuAnimations(connection.getMenuAnimation());
        performanceFlags.setTheming(connection.getVisualStyles());

        BookmarkBase.AdvancedSettings advancedSettings = session.getBookmark().getAdvancedSettings();
        advancedSettings.setRedirectSDCard(connection.getRedirectSdCard());
        advancedSettings.setConsoleMode(connection.getConsoleMode());
        advancedSettings.setRedirectSound(connection.getRemoteSoundType());
        advancedSettings.setRedirectMicrophone(connection.getEnableRecording());
        advancedSettings.setSecurity(0); // Automatic negotiation

        session.connect(this.getContext());
        pd.dismiss();
    }

    /**
     * Initializes a VNC connection.
     */
    private void initializeVncConnection() throws Exception {
        Log.i(TAG, "Initializing connection to: " + connection.getAddress() + ", port: " + connection.getPort());
        boolean sslTunneled = connection.getConnectionType() == Constants.CONN_TYPE_STUNNEL;
        rfb = new RfbProto(decoder, this, connection.getPrefEncoding(), connection.getViewOnly(),
                connection.getUseLocalCursor(), sslTunneled, connection.getIdHashAlgorithm(),
                connection.getIdHash(), connection.getSshHostKey());

        rfbconn = rfb;
        pointer = new RemoteVncPointer(rfbconn, RemoteCanvas.this, handler);
        boolean rAltAsIsoL3Shift = Utils.querySharedPreferenceBoolean(this.getContext(),
                Constants.rAltAsIsoL3ShiftTag);
        keyboard = new RemoteVncKeyboard(rfbconn, RemoteCanvas.this, handler, rAltAsIsoL3Shift);
    }

    /**
     * Starts a VNC connection using the TightVNC backend.
     */
    private void startVncConnection() throws Exception {

        try {
            String address = getAddress();
            int vncPort = getPort(connection.getPort());
            Log.i(TAG, "Establishing VNC session to: " + address + ", port: " + vncPort);
            // TODO: VNC Server cert is not set when the connection is SSH tunneled because there at
            // TODO: present it is assumed the connection is either SSH tunneled or x509 encrypted,
            // TODO: and when both are the case, there is no way to save the x509 cert.
            String sslCert = !sshTunneled? connection.getSshHostKey() : "";
            rfb.initializeAndAuthenticate(address, vncPort, connection.getUserName(),
                    connection.getPassword(), connection.getUseRepeater(),
                    connection.getRepeaterId(), connection.getConnectionType(),
                    sslCert);
        } catch (AnonCipherUnsupportedException e) {
            showFatalMessageAndQuit(getContext().getString(R.string.error_anon_dh_unsupported));
        } catch (Exception e) {
            e.printStackTrace();
            throw new Exception(getContext().getString(R.string.error_vnc_unable_to_connect) +
                    Utils.messageAndStackTraceAsString(e));
        }

        rfb.writeClientInit();
        rfb.readServerInit();

        // Is custom resolution enabled?
        if(connection.getRdpResType() != Constants.VNC_GEOM_SELECT_DISABLED) {
            waitUntilInflated();
            rfb.setPreferredFramebufferSize(getVncRemoteWidth(getWidth(), getHeight()),
                                            getVncRemoteHeight(getWidth(), getHeight()));
        }

        reallocateDrawable(displayWidth, displayHeight);
        decoder.setPixelFormat(rfb);

        handler.post(new Runnable() {
            public void run() {
                pd.setMessage(getContext().getString(R.string.info_progress_dialog_downloading));
            }
        });

        sendUnixAuth();
        if (connection.getUseLocalCursor())
            initializeSoftCursor();

        handler.post(drawableSetter);
        handler.post(setModes);

        // Hide progress dialog
        if (pd.isShowing())
            pd.dismiss();

        rfb.processProtocol();
    }


    /**
     * Sends over the unix username and password if this is VNC over SSH connectio and automatic sending of
     * UNIX credentials is enabled for AutoX (for x11vnc's "-unixpw" option).
     */
    void sendUnixAuth() {
        // If the type of connection is ssh-tunneled and we are told to send the unix credentials, then do so.
        if (sshTunneled && connection.getAutoXUnixAuth()) {
            keyboard.keyEvent(KeyEvent.KEYCODE_UNKNOWN, new KeyEvent(SystemClock.uptimeMillis(),
                    connection.getSshUser(), 0, 0));
            keyboard.keyEvent(KeyEvent.KEYCODE_ENTER, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
            keyboard.keyEvent(KeyEvent.KEYCODE_ENTER, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));

            keyboard.keyEvent(KeyEvent.KEYCODE_UNKNOWN, new KeyEvent(SystemClock.uptimeMillis(),
                    connection.getSshPassword(), 0, 0));
            keyboard.keyEvent(KeyEvent.KEYCODE_ENTER, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
            keyboard.keyEvent(KeyEvent.KEYCODE_ENTER, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
        }
    }

    /**
     * Retreives the requested remote width.
     */
    private int getRemoteWidth(int viewWidth, int viewHeight) {
        int remoteWidth = 0;
        int reqWidth = connection.getRdpWidth();
        int reqHeight = connection.getRdpHeight();
        if (connection.getRdpResType() == Constants.RDP_GEOM_SELECT_CUSTOM &&
                reqWidth >= 2 && reqHeight >= 2) {
            remoteWidth = reqWidth;
        } else if (connection.getRdpResType() == Constants.RDP_GEOM_SELECT_NATIVE_PORTRAIT) {
            remoteWidth = Math.min(viewWidth, viewHeight);
        } else {
            remoteWidth = Math.max(viewWidth, viewHeight);
        }
        // We make the resolution even if it is odd.
        if (remoteWidth % 2 == 1) remoteWidth--;
        return remoteWidth;
    }

    /**
     * Retreives the requested remote height.
     */
    private int getRemoteHeight(int viewWidth, int viewHeight) {
        int remoteHeight = 0;
        int reqWidth = connection.getRdpWidth();
        int reqHeight = connection.getRdpHeight();
        if (connection.getRdpResType() == Constants.RDP_GEOM_SELECT_CUSTOM &&
                reqWidth >= 2 && reqHeight >= 2) {
            remoteHeight = reqHeight;
        } else if (connection.getRdpResType() == Constants.RDP_GEOM_SELECT_NATIVE_PORTRAIT) {
            remoteHeight = Math.max(viewWidth, viewHeight);
        } else {
            remoteHeight = Math.min(viewWidth, viewHeight);
        }
        // We make the resolution even if it is odd.
        if (remoteHeight % 2 == 1) remoteHeight--;
        return remoteHeight;
    }

    /**
     * Determines the preferred remote width for VNC conncetions.
     */
    private int getVncRemoteWidth(int viewWidth, int viewHeight) {
        int remoteWidth = 0;
        int reqWidth = connection.getRdpWidth();
        int reqHeight = connection.getRdpHeight();
        if (connection.getRdpResType() == Constants.VNC_GEOM_SELECT_CUSTOM &&
                reqWidth >= 2 && reqHeight >= 2) {
            remoteWidth = reqWidth;
        } else if (connection.getRdpResType() == Constants.VNC_GEOM_SELECT_AUTOMATIC) {
            remoteWidth = viewWidth;
        } else if (connection.getRdpResType() == Constants.VNC_GEOM_SELECT_NATIVE_PORTRAIT) {
            remoteWidth = Math.min(viewWidth, viewHeight);
        } else if (connection.getRdpResType() == Constants.VNC_GEOM_SELECT_NATIVE_LANDSCAPE) {
            remoteWidth = Math.max(viewWidth, viewHeight);
        }
        // We make the resolution even if it is odd.
        if (remoteWidth % 2 == 1) remoteWidth--;
        return remoteWidth;
    }

    /**
     * Determines the preferred remote height for VNC conncetions.
     */
    private int getVncRemoteHeight(int viewWidth, int viewHeight) {
        int remoteHeight = 0;
        int reqWidth = connection.getRdpWidth();
        int reqHeight = connection.getRdpHeight();
        if (connection.getRdpResType() == Constants.VNC_GEOM_SELECT_CUSTOM &&
                reqWidth >= 2 && reqHeight >= 2) {
            remoteHeight = reqHeight;
        } else if (connection.getRdpResType() == Constants.VNC_GEOM_SELECT_AUTOMATIC) {
            remoteHeight = viewHeight;
        } else if (connection.getRdpResType() == Constants.VNC_GEOM_SELECT_NATIVE_PORTRAIT) {
            remoteHeight = Math.max(viewWidth, viewHeight);
        } else if (connection.getRdpResType() == Constants.VNC_GEOM_SELECT_NATIVE_LANDSCAPE) {
            remoteHeight = Math.min(viewWidth, viewHeight);
        }
        // We make the resolution even if it is odd.
        if (remoteHeight % 2 == 1) remoteHeight--;
        return remoteHeight;
    }

    /**
     * Shows a non-fatal error message.
     *
     * @param error
     */
    Runnable showDialogMessage = new Runnable() {
        public void run() {
            Utils.showErrorMessage(getContext(), String.valueOf(screenMessage));
        }
    };
    void showMessage(final String error) {
        android.util.Log.d(TAG, "showMessage");
        screenMessage = error;
        handler.removeCallbacks(showDialogMessage);
        handler.post(showDialogMessage);
    }

    /**
     * Closes the connection and shows a fatal message which ends the activity.
     *
     * @param error
     */
    void showFatalMessageAndQuit(final String error) {
        closeConnection();
        handler.post(new Runnable() {
            public void run() {
                Utils.showFatalErrorMessage(getContext(), error);
            }
        });
    }


    /**
     * If necessary, initializes an SSH tunnel and returns local forwarded port, or
     * if SSH tunneling is not needed, returns the given port.
     *
     * @return
     */
    int getPort(int port) throws Exception {
        int result = 0;

        if (sshTunneled) {
            if (sshConnection == null) {
                sshConnection = new SSHConnection(connection, getContext(), handler);
            }
            // TODO: Take the AutoX stuff out to a separate function.
            int newPort = sshConnection.initializeSSHTunnel();
            if (newPort > 0)
                port = newPort;
            result = sshConnection.createLocalPortForward(port);
        } else {
            if (isVnc && port <= 20) {
                result = Constants.DEFAULT_VNC_PORT + port;
            } else {
                result = port;
            }
        }
        return result;
    }


    /**
     * Returns localhost if using SSH tunnel or saved VNC address.
     *
     * @return
     */
    String getAddress() {
        if (sshTunneled) {
            return new String("127.0.0.1");
        } else
            return connection.getAddress();
    }


    /**
     * Initializes the drawable and bitmap into which the remote desktop is drawn.
     *
     * @param dx
     * @param dy
     * @throws IOException
     */
    @Override
    public void reallocateDrawable(int dx, int dy) {
        Log.i(TAG, "Desktop name is " + rfbconn.desktopName());
        Log.i(TAG, "Desktop size is " + rfbconn.framebufferWidth() + " x " + rfbconn.framebufferHeight());

        int fbsize = rfbconn.framebufferWidth() * rfbconn.framebufferHeight();

        capacity = BCFactory.getInstance().getBCActivityManager().getMemoryClass(Utils.getActivityManager(getContext()));

        if (connection.getForceFull() == BitmapImplHint.AUTO) {
            if (fbsize * CompactBitmapData.CAPACITY_MULTIPLIER <= capacity * 1024 * 1024) {
                useFull = true;
                compact = true;
            } else if (fbsize * FullBufferBitmapData.CAPACITY_MULTIPLIER <= capacity * 1024 * 1024) {
                useFull = true;
            } else {
                useFull = false;
            }
        } else {
            useFull = (connection.getForceFull() == BitmapImplHint.FULL);
        }

        if (!isVnc) {
            myDrawable = new UltraCompactBitmapData(rfbconn, this, isSpice);
            android.util.Log.i(TAG, "Using UltraCompactBufferBitmapData.");
        } else if (!useFull) {
            myDrawable = new LargeBitmapData(rfbconn, this, dx, dy, capacity);
            android.util.Log.i(TAG, "Using LargeBitmapData.");
        } else {
            try {
                // TODO: Remove this if Android 4.2 receives a fix for a bug which causes it to stop drawing
                // the bitmap in CompactBitmapData when under load (say playing a video over VNC).
                if (!compact) {
                    myDrawable = new FullBufferBitmapData(rfbconn, this, capacity);
                    android.util.Log.i(TAG, "Using FullBufferBitmapData.");
                } else {
                    myDrawable = new CompactBitmapData(rfbconn, this, isSpice);
                    android.util.Log.i(TAG, "Using CompactBufferBitmapData.");
                }
            } catch (Throwable e) { // If despite our efforts we fail to allocate memory, use LBBM.
                disposeDrawable();

                useFull = false;
                myDrawable = new LargeBitmapData(rfbconn, this, dx, dy, capacity);
                android.util.Log.i(TAG, "Using LargeBitmapData.");
            }
        }

        try {
            initializeSoftCursor();
            handler.post(drawableSetter);
            handler.post(setModes);
            myDrawable.syncScroll();
            decoder.setBitmapData(myDrawable);
        } catch (NullPointerException e) {
            e.printStackTrace();
        }
    }


    /**
     * Disposes of the old drawable which holds the remote desktop data.
     */
    private void disposeDrawable() {
        if (myDrawable != null)
            myDrawable.dispose();
        myDrawable = null;
        System.gc();
    }


    /**
     * The remote desktop's size has changed and this method
     * reinitializes local data structures to match.
     */
    public void updateFBSize() {
        try {
            myDrawable.frameBufferSizeChanged();
        } catch (Throwable e) {
            boolean useLBBM = false;

            // If we've run out of memory, try using another bitmapdata type.
            if (e instanceof OutOfMemoryError) {
                disposeDrawable();

                // If we were using CompactBitmapData, try FullBufferBitmapData.
                if (compact == true) {
                    compact = false;
                    try {
                        myDrawable = new FullBufferBitmapData(rfbconn, this, capacity);
                    } catch (Throwable e2) {
                        useLBBM = true;
                    }
                } else
                    useLBBM = true;

                // Failing FullBufferBitmapData or if we weren't using CompactBitmapData, try LBBM.
                if (useLBBM) {
                    disposeDrawable();

                    useFull = false;
                    myDrawable = new LargeBitmapData(rfbconn, this, getWidth(), getHeight(), capacity);
                }
                decoder.setBitmapData(myDrawable);
            }
        }
        handler.post(drawableSetter);
        handler.post(setModes);
        myDrawable.syncScroll();
    }


    /**
     * Displays a short toast message on the screen.
     *
     * @param message
     */
    public void displayShortToastMessage(final CharSequence message) {
        screenMessage = message;
        handler.removeCallbacks(showMessage);
        handler.post(showMessage);
    }


    /**
     * Displays a short toast message on the screen.
     *
     * @param messageID
     */
    public void displayShortToastMessage(final int messageID) {
        screenMessage = getResources().getText(messageID);
        handler.removeCallbacks(showMessage);
        handler.post(showMessage);
    }


    /**
     * Lets the drawable know that an update from the remote server has arrived.
     */
    public void doneWaiting() {
        myDrawable.doneWaiting();
    }


    /**
     * Indicates that RemoteCanvas's scroll position should be synchronized with the
     * drawable's scroll position (used only in LargeBitmapData)
     */
    public void syncScroll() {
        myDrawable.syncScroll();
    }


    /**
     * Requests a remote desktop update at the specified rectangle.
     */
    public void writeFramebufferUpdateRequest(int x, int y, int w, int h, boolean incremental) throws IOException {
        myDrawable.prepareFullUpdateRequest(incremental);
        rfbconn.writeFramebufferUpdateRequest(x, y, w, h, incremental);
    }


    /**
     * Requests an update of the entire remote desktop.
     */
    public void writeFullUpdateRequest(boolean incremental) {
        myDrawable.prepareFullUpdateRequest(incremental);
        rfbconn.writeFramebufferUpdateRequest(myDrawable.getXoffset(), myDrawable.getYoffset(),
                myDrawable.bmWidth(), myDrawable.bmHeight(), incremental);
    }


    /**
     * Set the device clipboard text with the string parameter.
     */
    public void setClipboardText(String s) {
        if (s != null && s.length() > 0) {
            clipboard.setText(s);
        }
    }


    /**
     * Method that disconnects from the remote server.
     */
    public void closeConnection() {
        maintainConnection = false;

        if (keyboard != null) {
            // Tell the server to release any meta keys.
            keyboard.clearMetaState();
            keyboard.keyEvent(0, new KeyEvent(KeyEvent.ACTION_UP, 0));
        }
        // Close the rfb connection.
        if (rfbconn != null) {
            rfbconn.close();
        }

        if (handler != null) {
            handler.removeCallbacksAndMessages(null);
        }

        // Close the SSH tunnel.
        if (sshConnection != null) {
            sshConnection.terminateSSHTunnel();
            sshConnection = null;
        }
        onDestroy();
    }


    /**
     * Cleans up resources after a disconnection.
     */
    public void onDestroy() {
        Log.v(TAG, "Cleaning up resources");

        removeCallbacksAndMessages();
        if (clipboardMonitorTimer != null) {
            clipboardMonitorTimer.cancel();
            // Occasionally causes a NullPointerException
            //clipboardMonitorTimer.purge();
            clipboardMonitorTimer = null;
        }
        clipboardMonitor = null;
        clipboard = null;
        setModes = null;
        decoder = null;
        canvasZoomer = null;
        drawableSetter = null;
        screenMessage = null;
        desktopInfo = null;

        disposeDrawable();
    }


    public void removeCallbacksAndMessages() {
        if (handler != null) {
            handler.removeCallbacksAndMessages(null);
        }
    }
    
    /*
     * f(x,s) is a function that returns the coordinate in screen/scroll space corresponding
     * to the coordinate x in full-frame space with scaling s.
     * 
     * This function returns the difference between f(x,s1) and f(x,s2)
     * 
     * f(x,s) = (x - i/2) * s + ((i - w)/2)) * s
     *        = s (x - i/2 + i/2 + w/2)
     *        = s (x + w/2)
     * 
     * 
     * f(x,s) = (x - ((i - w)/2)) * s
     * @param oldscaling
     * @param scaling
     * @param imageDim
     * @param windowDim
     * @param offset
     * @return
     */

    /**
     * Computes the X and Y offset for converting coordinates from full-frame coordinates to view coordinates.
     */
    public void computeShiftFromFullToView() {
        shiftX = (rfbconn.framebufferWidth() - getWidth()) / 2;
        shiftY = (rfbconn.framebufferHeight() - getHeight()) / 2;
    }

    /**
     * Change to Canvas's scroll position to match the absoluteXPosition
     */
    void resetScroll() {
        float scale = getZoomFactor();
        //android.util.Log.d(TAG, "resetScroll: " + (absoluteXPosition - shiftX) * scale + ", "
        //                                        + (absoluteYPosition - shiftY) * scale);
        scrollTo((int) ((absoluteXPosition - shiftX) * scale),
                (int) ((absoluteYPosition - shiftY) * scale));
    }


    /**
     * Make sure mouse is visible on displayable part of screen
     */
    public void movePanToMakePointerVisible() {
        if (rfbconn == null)
            return;

        boolean panX = true;
        boolean panY = true;

        // Don't pan in a certain direction if dimension scaled is already less 
        // than the dimension of the visible part of the screen.
        if (rfbconn.framebufferWidth() <= getVisibleWidth())
            panX = false;
        if (rfbconn.framebufferHeight() <= getVisibleHeight())
            panY = false;

        // We only pan if the current scaling is able to pan.
        if (canvasZoomer != null && !canvasZoomer.isAbleToPan())
            return;

        int x = pointer.getX();
        int y = pointer.getY();
        boolean panned = false;
        int w = getVisibleWidth();
        int h = getVisibleHeight();
        int iw = getImageWidth();
        int ih = getImageHeight();
        int wthresh = 30;
        int hthresh = 30;

        int newX = absoluteXPosition;
        int newY = absoluteYPosition;

        if (x - absoluteXPosition >= w - wthresh) {
            newX = x - (w - wthresh);
            if (newX + w > iw)
                newX = iw - w;
        } else if (x < absoluteXPosition + wthresh) {
            newX = x - wthresh;
            if (newX < 0)
                newX = 0;
        }
        if (panX && newX != absoluteXPosition) {
            absoluteXPosition = newX;
            panned = true;
        }

        if (y - absoluteYPosition >= h - hthresh) {
            newY = y - (h - hthresh);
            if (newY + h > ih)
                newY = ih - h;
        } else if (y < absoluteYPosition + hthresh) {
            newY = y - hthresh;
            if (newY < 0)
                newY = 0;
        }
        if (panY && newY != absoluteYPosition) {
            absoluteYPosition = newY;
            panned = true;
        }

        if (panned) {
            //scrollBy(newX - absoluteXPosition, newY - absoluteYPosition);
            resetScroll();
        }
    }

    /**
     * Pan by a number of pixels (relative pan)
     *
     * @param dX
     * @param dY
     * @return True if the pan changed the view (did not move view out of bounds); false otherwise
     */
    public boolean relativePan(int dX, int dY) {
        //android.util.Log.d(TAG, "relativePan: " + dX + ", " + dY);

        // We only pan if the current scaling is able to pan.
        if (canvasZoomer != null && !canvasZoomer.isAbleToPan())
            return false;

        double scale = getZoomFactor();

        double sX = (double) dX / scale;
        double sY = (double) dY / scale;

        if (absoluteXPosition + sX < 0)
            // dX = diff to 0
            sX = -absoluteXPosition;
        if (absoluteYPosition + sY < 0)
            sY = -absoluteYPosition;

        // Prevent panning right or below desktop image
        if (absoluteXPosition + getVisibleWidth() + sX > getImageWidth())
            sX = getImageWidth() - getVisibleWidth() - absoluteXPosition;
        if (absoluteYPosition + getVisibleHeight() + sY > getImageHeight())
            sY = getImageHeight() - getVisibleHeight() - absoluteYPosition;

        absoluteXPosition += sX;
        absoluteYPosition += sY;
        if (sX != 0.0 || sY != 0.0) {
            //scrollBy((int)sX, (int)sY);
            resetScroll();
            return true;
        }
        return false;
    }

    /**
     * Absolute pan.
     *
     * @param x
     * @param y
     */
    public void absolutePan(int x, int y) {
        //android.util.Log.d(TAG, "absolutePan: " + x + ", " + y);

        if (canvasZoomer != null) {
            int vW = getVisibleWidth();
            int vH = getVisibleHeight();
            int w = getImageWidth();
            int h = getImageHeight();
            if (x + vW > w) x = w - vW;
            if (y + vH > h) y = h - vH;
            if (x < 0) x = 0;
            if (y < 0) y = 0;
            absoluteXPosition = x;
            absoluteYPosition = y;
            resetScroll();
        }
    }

    /* (non-Javadoc)
     * @see android.view.View#onScrollChanged(int, int, int, int)
     */
    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        if (myDrawable != null) {
            myDrawable.scrollChanged(absoluteXPosition, absoluteYPosition);
            pointer.movePointerToMakeVisible();
        }
    }


    /**
     * This runnable sets the drawable (contained in myDrawable) for the VncCanvas (ImageView).
     */
    private Runnable drawableSetter = new Runnable() {
        public void run() {
            if (myDrawable != null)
                myDrawable.setImageDrawable(RemoteCanvas.this);
        }
    };


    /**
     * This runnable displays a message on the screen.
     */
    CharSequence screenMessage;
    private Runnable showMessage = new Runnable() {
        public void run() {
            Toast.makeText(getContext(), screenMessage, Toast.LENGTH_SHORT).show();
        }
    };


    /**
     * This runnable causes a toast with information about the current connection to be shown.
     */
    private Runnable desktopInfo = new Runnable() {
        public void run() {
            showConnectionInfo();
        }
    };

    @Override
    public Bitmap getBitmap() {
        Bitmap bitmap = null;
        if (myDrawable != null) {
            bitmap = myDrawable.mbitmap;
        }
        return bitmap;
    }

    /**
     * Causes a redraw of the myDrawable to happen at the indicated coordinates.
     */
    public void reDraw(int x, int y, int w, int h) {
        //android.util.Log.i(TAG, "reDraw called: " + x +", " + y + " + " + w + "x" + h);
        float scale = getZoomFactor();
        float shiftedX = x - shiftX;
        float shiftedY = y - shiftY;
        // Make the box slightly larger to avoid artifacts due to truncation errors.
        postInvalidate((int) ((shiftedX - 1) * scale), (int) ((shiftedY - 1) * scale),
                (int) ((shiftedX + w + 1) * scale), (int) ((shiftedY + h + 1) * scale));
    }


    /**
     * This is a float-accepting version of reDraw().
     * Causes a redraw of the myDrawable to happen at the indicated coordinates.
     */
    public void reDraw(float x, float y, float w, float h) {
        float scale = getZoomFactor();
        float shiftedX = x - shiftX;
        float shiftedY = y - shiftY;
        // Make the box slightly larger to avoid artifacts due to truncation errors.
        postInvalidate((int) ((shiftedX - 1.f) * scale), (int) ((shiftedY - 1.f) * scale),
                (int) ((shiftedX + w + 1.f) * scale), (int) ((shiftedY + h + 1.f) * scale));
    }

    /**
     * Displays connection info in a toast message.
     */
    public void showConnectionInfo() {
        if (rfbconn == null)
            return;

        String msg = null;
        int idx = rfbconn.desktopName().indexOf("(");
        if (idx > 0) {
            // Breakup actual desktop name from IP addresses for improved
            // readability
            String dn = rfbconn.desktopName().substring(0, idx).trim();
            String ip = rfbconn.desktopName().substring(idx).trim();
            msg = dn + "\n" + ip;
        } else
            msg = rfbconn.desktopName();
        msg += "\n" + rfbconn.framebufferWidth() + "x" + rfbconn.framebufferHeight();
        String enc = rfbconn.getEncoding();
        // Encoding might not be set when we display this message
        if (decoder.getColorModel() != null) {
            if (enc != null && !enc.equals(""))
                msg += ", " + rfbconn.getEncoding() + getContext().getString(R.string.info_encoding) + decoder.getColorModel().toString();
            else
                msg += ", " + decoder.getColorModel().toString();
        }
        Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
    }


    /**
     * Invalidates (to redraw) the location of the remote pointer.
     */
    public void invalidateMousePosition() {
        if (myDrawable != null) {
            myDrawable.moveCursorRect(pointer.getX(), pointer.getY());
            RectF r = myDrawable.getCursorRect();
            reDraw(r.left, r.top, r.width(), r.height());
        }
    }

    @Override
    public void setMousePointerPosition(int x, int y) {
        softCursorMove(x, y);
    }

    @Override
    public void mouseMode(boolean relative) {
        if (relative && !connection.getInputMode().equals(InputHandlerTouchpad.ID)) {
            showMessage(getContext().getString(R.string.info_set_touchpad_input_mode));
        } else {
            this.pointer.setRelativeEvents(relative);
        }
    }

    /**
     * Moves soft cursor into a particular location.
     *
     * @param x
     * @param y
     */
    synchronized void softCursorMove(int x, int y) {
        if (myDrawable.isNotInitSoftCursor()) {
            initializeSoftCursor();
        }

        if (!cursorBeingMoved || pointer.isRelativeEvents()) {
            pointer.setX(x);
            pointer.setY(y);
            RectF prevR = new RectF(myDrawable.getCursorRect());
            // Move the cursor.
            myDrawable.moveCursorRect(x, y);
            // Show the cursor.
            RectF r = myDrawable.getCursorRect();
            reDraw(r.left, r.top, r.width(), r.height());
            reDraw(prevR.left, prevR.top, prevR.width(), prevR.height());
        }
    }


    /**
     * Initializes the data structure which holds the remote pointer data.
     */
    void initializeSoftCursor() {
        Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.cursor);
        int w = bm.getWidth();
        int h = bm.getHeight();
        int[] tempPixels = new int[w * h];
        bm.getPixels(tempPixels, 0, w, 0, 0, w, h);
        // Set cursor rectangle as well.
        myDrawable.setCursorRect(pointer.getX(), pointer.getY(), w, h, 0, 0);
        // Set softCursor to whatever the resource is.
        myDrawable.setSoftCursor(tempPixels);
        bm.recycle();
    }

    @Override
    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
        android.util.Log.d(TAG, "onCreateInputConnection called");
        BaseInputConnection bic = new BaseInputConnection(this, false);
        outAttrs.actionLabel = null;
        outAttrs.inputType = InputType.TYPE_NULL;
        String currentIme = Settings.Secure.getString(getContext().getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD);
        android.util.Log.d(TAG, "currentIme: " + currentIme);
        outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN;
        return bic;
    }

    public RemotePointer getPointer() {
        return pointer;
    }

    public RemoteKeyboard getKeyboard() {
        return keyboard;
    }

    public float getZoomFactor() {
        if (canvasZoomer == null)
            return 1;
        return canvasZoomer.getZoomFactor();
    }

    public int getVisibleWidth() {
        return (int) ((double) getWidth() / getZoomFactor() + 0.5);
    }

    public void setVisibleHeight(int newHeight) {
        visibleHeight = newHeight;
    }

    public int getVisibleHeight() {
        if (visibleHeight > 0)
            return (int) ((double) visibleHeight / getZoomFactor() + 0.5);
        else
            return (int) ((double) getHeight() / getZoomFactor() + 0.5);
    }

    public int getImageWidth() {
        return rfbconn.framebufferWidth();
    }

    public int getImageHeight() {
        return rfbconn.framebufferHeight();
    }

    public int getCenteredXOffset() {
        return (rfbconn.framebufferWidth() - getWidth()) / 2;
    }

    public int getCenteredYOffset() {
        return (rfbconn.framebufferHeight() - getHeight()) / 2;
    }

    public float getMinimumScale() {
        if (myDrawable != null) {
            return myDrawable.getMinimumScale();
        } else
            return 1.f;
    }

    public float getDisplayDensity() {
        return displayDensity;
    }

    public boolean isColorModel(COLORMODEL cm) {
        return (decoder.getColorModel() != null) && decoder.getColorModel().equals(cm);
    }

    public void setColorModel(COLORMODEL cm) {
        decoder.setColorModel(cm);
    }

    public boolean getMouseFollowPan() {
        return connection.getFollowPan();
    }

    public int getAbsX() {
        return absoluteXPosition;
    }

    public int getAbsY() {
        return absoluteYPosition;
    }

    /**
     * Used to wait until getWidth and getHeight return sane values.
     */
    public void waitUntilInflated() {
        synchronized (this) {
            while (getWidth() == 0 || getHeight() == 0) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * Used to detect when the view is inflated to a sane size other than 0x0.
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        if (w > 0 && h > 0) {
            synchronized (this) {
                this.notify();
            }
        }
    }

    /**
     * Handler for the dialogs that display the x509/RDP/SSH key signatures to the user.
     * Also shows the dialogs which show various connection failures.
     */
    public Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            FragmentManager fm = null;
            Bundle s = null;

            android.util.Log.d(TAG, "Handling message, msg.what: " + msg.what);

            switch (msg.what) {
                case Constants.PRO_FEATURE:
                    if (pd != null && pd.isShowing()) {
                        pd.dismiss();
                    }
                    showFatalMessageAndQuit(getContext().getString(R.string.pro_feature_mfa));
                    break;
                case Constants.GET_VERIFICATIONCODE:
                    if (pd != null && pd.isShowing()) {
                        pd.dismiss();
                    }
                    fm = ((FragmentActivity) getContext()).getSupportFragmentManager();
                    GetTextFragment getPassword = GetTextFragment.newInstance(
                            RemoteCanvas.this.getContext().getString(R.string.verification_code),
                            sshConnection, GetTextFragment.Plaintext, R.string.verification_code_message, R.string.verification_code);
                    getPassword.setCancelable(false);
                    getPassword.show(fm, RemoteCanvas.this.getContext().getString(R.string.verification_code));
                    break;
                case Constants.DIALOG_X509_CERT:
                    validateX509Cert((X509Certificate) msg.obj);
                    break;
                case Constants.DIALOG_SSH_CERT:
                    initializeSshHostKey();
                    break;
                case Constants.DIALOG_RDP_CERT:
                    s = (Bundle) msg.obj;
                    validateRdpCert(s.getString("subject"), s.getString("issuer"), s.getString("fingerprint"));
                    break;
                case Constants.SPICE_CONNECT_SUCCESS:
                    if (pd != null && pd.isShowing()) {
                        pd.dismiss();
                    }
                    break;
                case Constants.SPICE_CONNECT_FAILURE:
                    if (maintainConnection) {
                        if (pd != null && pd.isShowing()) {
                            pd.dismiss();
                        }
                        if (!spiceUpdateReceived) {
                            showFatalMessageAndQuit(getContext().getString(R.string.error_spice_unable_to_connect));
                        } else {
                            showFatalMessageAndQuit(getContext().getString(R.string.error_connection_interrupted));
                        }
                    }
                    break;
                case Constants.RDP_CONNECT_FAILURE:
                    if (maintainConnection) {
                        showFatalMessageAndQuit(getContext().getString(R.string.error_rdp_connection_failed));
                    }
                    break;
                case Constants.RDP_UNABLE_TO_CONNECT:
                    if (maintainConnection) {
                        showFatalMessageAndQuit(getContext().getString(R.string.error_rdp_unable_to_connect));
                    }
                    break;
                case Constants.RDP_AUTH_FAILED:
                    if (maintainConnection) {
                        showFatalMessageAndQuit(getContext().getString(R.string.error_rdp_authentication_failed));
                    }
                    break;
                case Constants.SERVER_CUT_TEXT:
                    s = (Bundle) msg.obj;
                    serverJustCutText = true;
                    setClipboardText(s.getString("text"));
                    break;
            }
        }
    };

    /**
     * If there is a saved cert, checks the one given against it. If a signature was passed in
     * and no saved cert, then check that signature. Otherwise, presents the
     * given cert's signature to the user for approval.
     * <p>
     * The saved data must always win over any passed-in URI data
     *
     * @param cert the given cert.
     */
    @SuppressLint("StringFormatInvalid")
    private void validateX509Cert(final X509Certificate cert) {
        android.util.Log.d(TAG, "Displaying dialog to validate X509 Cert");
        boolean certMismatch = false;

        int hashAlg = connection.getIdHashAlgorithm();
        byte[] certData = null;
        boolean isSigEqual = false;
        try {
            certData = cert.getEncoded();
            isSigEqual = SecureTunnel.isSignatureEqual(hashAlg, connection.getIdHash(), certData);
        } catch (Exception ex) {
            ex.printStackTrace();
            showFatalMessageAndQuit(getContext().getString(R.string.error_x509_could_not_generate_signature));
            return;
        }

        // If there is no saved cert, then if a signature was provided,
        // check the signature and save the cert if the signature matches.
        if (connection.getSshHostKey().equals("")) {
            if (!connection.getIdHash().equals("")) {
                if (isSigEqual) {
                    Log.i(TAG, "Certificate validated from URI data.");
                    saveAndAcceptCert(cert);
                    return;
                } else {
                    certMismatch = true;
                }
            }
            // If there is a saved cert, check against it.
        } else if (connection.getSshHostKey().equals(Base64.encodeToString(certData, Base64.DEFAULT))) {
            Log.i(TAG, "Certificate validated from saved key.");
            saveAndAcceptCert(cert);
            return;
        } else if (sshTunneled) {
            Log.i(TAG, "X509 connection tunneled over SSH, so we have no place to save the cert fingerprint.");
        } else {
            certMismatch = true;
        }

        // Show a dialog with the key signature for approval.
        DialogInterface.OnClickListener signatureNo = new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // We were told not to continue, so stop the activity
                Log.i(TAG, "Certificate rejected by user.");
                closeConnection();
                ((Activity) getContext()).finish();
            }
        };
        DialogInterface.OnClickListener signatureYes = new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                Log.i(TAG, "Certificate accepted by user.");
                saveAndAcceptCert(cert);
            }
        };

        // Display dialog to user with cert info and hash.
        try {
            // First build the message. If there was a mismatch, prepend a warning about it.
            String message = "";
            if (certMismatch) {
                message = getContext().getString(R.string.warning_cert_does_not_match) + "\n\n";
            }
            byte[] certBytes = cert.getEncoded();
            String certIdHash = SecureTunnel.computeSignatureByAlgorithm(hashAlg, certBytes);
            String certInfo =
                    String.format(Locale.US, getContext().getString(R.string.info_cert_tunnel),
                            certIdHash,
                            cert.getSubjectX500Principal().getName(),
                            cert.getIssuerX500Principal().getName(),
                            cert.getNotBefore(),
                            cert.getNotAfter()
                    );
            certInfo = message + certInfo.replace(",", "\n");

            // Actually display the message
            Utils.showYesNoPrompt(getContext(),
                    getContext().getString(R.string.info_continue_connecting) + connection.getAddress() + "?",
                    certInfo,
                    signatureYes, signatureNo);
        } catch (NoSuchAlgorithmException e2) {
            e2.printStackTrace();
            showFatalMessageAndQuit(getContext().getString(R.string.error_x509_could_not_generate_signature));
        } catch (CertificateEncodingException e) {
            e.printStackTrace();
            showFatalMessageAndQuit(getContext().getString(R.string.error_x509_could_not_generate_encoding));
        }
    }

    /**
     * Saves and accepts a x509 certificate.
     *
     * @param cert
     */
    private void saveAndAcceptCert(X509Certificate cert) {
        if (!sshTunneled) {
            android.util.Log.d(TAG, "Saving X509 cert fingerprint.");
            String certificate = null;
            try {
                certificate = Base64.encodeToString(cert.getEncoded(), Base64.DEFAULT);
            } catch (CertificateEncodingException e) {
                e.printStackTrace();
                showFatalMessageAndQuit(getContext().getString(R.string.error_x509_could_not_generate_encoding));
            }
            connection.setSshHostKey(certificate);
            connection.save(database.getWritableDatabase());
            database.close();
        } else {
            android.util.Log.d(TAG, "Not saving X509 cert fingerprint because connection is SSH tunneled.");
        }
        // Indicate the certificate was accepted.
        rfb.setCertificateAccepted(true);
        synchronized (rfb) {
            rfb.notifyAll();
        }
    }

    /**
     * Permits the user to validate an RDP certificate.
     *
     * @param subject
     * @param issuer
     * @param fingerprint
     */
    private void validateRdpCert(String subject, String issuer, final String fingerprint) {
        // Since LibFreeRDP handles saving accepted certificates, if we ever get here, we must
        // present the user with a query whether to accept the certificate or not.

        // Show a dialog with the key signature for approval.
        DialogInterface.OnClickListener signatureNo = new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // We were told not to continue, so stop the activity
                closeConnection();
                ((Activity) getContext()).finish();
            }
        };
        DialogInterface.OnClickListener signatureYes = new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // Indicate the certificate was accepted.
                rfbconn.setCertificateAccepted(true);
                synchronized (rfbconn) {
                    rfbconn.notifyAll();
                }
            }
        };
        Utils.showYesNoPrompt(getContext(), getContext().getString(R.string.info_continue_connecting) + connection.getAddress() + "?",
                getContext().getString(R.string.info_cert_signatures) +
                        "\nSubject:      " + subject +
                        "\nIssuer:       " + issuer +
                        "\nFingerprint:  " + fingerprint +
                        getContext().getString(R.string.info_cert_signatures_identical),
                signatureYes, signatureNo);
    }


    /**
     * Function used to initialize an empty SSH HostKey for a new VNC over SSH connection.
     */
    private void initializeSshHostKey() {
        // If the SSH HostKey is empty, then we need to grab the HostKey from the server and save it.
        Log.d(TAG, "Attempting to initialize SSH HostKey.");

        displayShortToastMessage(getContext().getString(R.string.info_ssh_initializing_hostkey));

        sshConnection = new SSHConnection(connection, getContext(), handler);
        if (!sshConnection.connect()) {
            // Failed to connect, so show error message and quit activity.
            showFatalMessageAndQuit(getContext().getString(R.string.error_ssh_unable_to_connect));
        } else {
            // Show a dialog with the key signature.
            DialogInterface.OnClickListener signatureNo = new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    // We were told to not continue, so stop the activity
                    sshConnection.terminateSSHTunnel();
                    pd.dismiss();
                    ((Activity) getContext()).finish();
                }
            };
            DialogInterface.OnClickListener signatureYes = new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    // We were told to go ahead with the connection.
                    connection.setIdHash(sshConnection.getIdHash()); // could prompt based on algorithm
                    connection.setSshHostKey(sshConnection.getServerHostKey());
                    connection.save(database.getWritableDatabase());
                    database.close();
                    sshConnection.terminateSSHTunnel();
                    sshConnection = null;
                    synchronized (RemoteCanvas.this) {
                        RemoteCanvas.this.notify();
                    }
                }
            };

            Utils.showYesNoPrompt(getContext(),
                    getContext().getString(R.string.info_continue_connecting) + connection.getSshServer() + "?",
                    getContext().getString(R.string.info_ssh_key_fingerprint) + sshConnection.getHostKeySignature() +
                            getContext().getString(R.string.info_ssh_key_fingerprint_identical),
                    signatureYes, signatureNo);
        }
    }
}
